Form 데이터의 클라이언트 사이드 검증과 서버 사이드 검증
✒️ 2025-06-25 17:33 내용 수정
클라이언트 사이드 검증
참고 자료 : Craig Buckler's Form Validation Using JavaScript's Constraint Validation API, Geeksforgeeks JavaScript Form Validation, mdn web docs Client-side form validation
- 사용자로부터 데이터를 받을 때 입력 데이터가 적합하거나, 빈 값인지 등을 확인해야 한다.
- 잘못된 데이터를 서버가 수신한다면 에러가 발생하거나 의도치 않은 결과를 만들 수 있다.
- 클라이언트에서 이를 검증할 때는 JavaScript를 사용하여
form의 데이터를 서버로 전송하기 전에 유효성 검사를 실행한다. input에 기본 유효성 검증 기능들이 있어 이를 이용해regex나 필수 입력 등을 설정할 수 있다.
function validate(e) {
const email = document.querySelector("#email").value.trim();
const password = document.querySelector("#password").value;
const emailError = document.querySelector("#emailError");
const passwordError = document.querySelector("#passwordError");
// 이메일 정규표현식
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// 빈 값 확인
if (!email || email === "" || /\d/.test(email)) {
e.preventDefault();
emailError.textContent = "이메일을 입력해주세요";
return false;
}
// 형식 확인
if (!emailRegex.test(email)) {
e.preventDefault();
emailError.textContent =
"유효한 비밀번호 주소를 입력해주세요";
return false;
}
// 비밀번호 정규표현식
// 8~16자, 숫자 1개 포함
const passRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/;
if (!password || password === "" || /\d/.test(password)) {
e.preventDefault();
passwordError.textContent = "비밀번호를 입력해주세요";
return false;
}
if (!passRegex.test(password)) {
e.preventDefault();
passwordError.textContent =
"비밀번호는 8~16자, 최소 숫자 1개를 포함해야 합니다";
return false;
}
return true;
}
<form action="/login" method="post"
onsubmit="return validate(e)">
<label for="email">이메일</label>
<input type="email"
name="email" id="email"
required>
<span id="emailError" sytle="color:red;"></span>
<label for="password">비밀번호</label>
<input type="password"
name="password" id="password"
required>
<span id="passwordError" sytle="color:red;"></span>
<button type="submit">제출</button>
</form>
클라이언트 검증의 한계
-
그러나 클라이언트 검증은 사용자가 검증을 우회하거나 검증 자체가 동작하지 않는 경우가 있기에 서버에서도 한 번 더 검증을 거쳐야 한다.
- 웹 브라우저가 JavaScript를 지원하지 않거나 브라우저에 따른 JavaScript 동작이 다른 경우
- 사용자가 웹 브라우저의 JavaScript를 비활성화한 경우
- 사용자가 개발자 도구에서 검증과 관련된 JavaScript를 변경하거나 HTML 속성을 수정하는 경우
- 웹 페이지가 아닌 Postman 등의 도구를 사용하여 서버에 직접 요청을 보내는 경우
-
서버에서 추가 검증한다면 클라이언트에서 검증할 필요가 있을지 의문이 들 수 있다.
- 클라이언트에서 검증을 수행하면 사용자에게 어떤 입력 값이 잘못 되었는지 바로 알려줄 수 있기에 UI/UX 측면에서 필요하다.
- 클라이언트 검증 없이 바로 서버에 전송 후 사용자에게 잘못된 값을 다시 받는 데에도 시간이 걸리기 때문에 클라이언트에서 검증을 끝낸 후에 보내는 것이 좋다.
서버 사이드 검증
- 클라이언트로부터 온 데이터를 서버에서 한 번 더 검증해야 한다.
- 잘못된 데이터가 Database에 들어오는 것을 막아야 한다.
- 중요한 비즈니스 로직이 위반되지 않도록 보장해야 한다.
- XSS, CSRF, SQL Injection 등의 보안 공격에 대비해야 한다.
데이터 무결성
- 입력 값의 타입과 Database에 저장된 Column의 타입이 일치하는지 확인한다.
public class User {
private String name;
private int age;
}
요청 : {name: "홍길동", age: "abc"} => X
요청 : {name: "홍길동", age: 20} => O
- 필수적인 데이터가 빈 값으로 오거나 빠져 있는지 확인한다.
요청 : {age: 20} => X
요청 : {name: "홍길동", age: 20} => O
- 비즈니스 로직에 따라 데이터의 길이나 크기를 제한 및 확인한다.
- overflow 또는 underflow 문제가 발생할 수 있다.
나이 : 0 ~ 150
요청 : {name: "홍길동", age : -1} => X
요청 : {name: "홍길동", age : 20} => O
커스텀 에러 메시지 사용
- 에러 처리 시 에러 메시지를 사용자에게 노출하지 않고 내부적으로만 처리해야 한다.
- 에러 메시지가 노출되면 핵심 비즈니스 로직의 취약점을 외부에 공개하게 되는 불상사가 발생할 수 있다.
try {
test();
} catch(Exception e) {
// 에러 로그는 내부에만 출력
System.out.println("에러 발생: " + e.getMessage());
// 에러 발생 시 커스텀 에러 메시지를 따로 지정
request.setAttribute("errorMessage", "에러가 발생했습니다.");
}
XSS 공격 방지
- XSS(Cross Site Scripting) : 사용자가 웹 사이트에 악성 Script를 삽입할 수 있는 보안 공격이다.
- 악성 Script 실행 시 다른 사용자의 Cookie나 Session, 중요 정보 등이 유출될 수 있다.
- 웹 페이지에 스크립트가 포함된 내용이 업로드 되지 않도록 방지하기 위해 HTML 태그 및 스크립트를 제거한다.
- Node의 sanitize-html와 같은 라이브러리에서도 스크립트를 제거하는 메서드를 제공한다.
package util;
import java.security.SecureRandom;
import java.util.regex.Pattern;
public class SecurityUtil {
private static final Pattern SCRIPT_PATTERN =
Pattern.compile("(?i)<script[^>]*>.*?</script>");
// XSS 방지 - HTML 태그 제거 및 이스케이프
public static String escapeHtml(String input) {
if (input == null) return null;
return input.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
.replace("/", "/");
}
// 스크립트 태그 제거
public static String removeScripts(String input) {
if (input == null) return null;
// Pattern과 Matcher를 사용한 정규 표현식 검증
// script 태그가 포함된 경우 제거하기
return SCRIPT_PATTERN.matcher(input).replaceAll("");
}
}
- 또한 Cookie 사용 시에
setHttpOnly()메서드로 JavaScript를 통한 Cookie 접근을 차단해야 한다.- Script를 사용하여 사용자의 Cookie나 Session 데이터를 탈취할 수 있기에 JavaScript 접근을 차단한다.
User user = new User("홍길동");
Cookie cookie = new Cookie("user", user);
cookie.setHttpOnly(true); // JavaScript 접근 차단
cookie.setSecure(true); // HTTPS 전용
response.addCookie(cookie); // 응답 객체에 cookie 추가
CSRF 공격 방지
- CSRF(Cross Site Request Forgery) : 사용자가 의도치 않게 공격자의 의도대로 행동하게 만드는 공격이다.
- 사용자의 의지와 상관 없이 데이터를 전송하는 공격 등이 있다.
- 사용자가 요청을 보낸 것이 자신임을 증명하기 위한 방법을 설정한다.
- 랜덤한 문자열 등으로 CSRF Token을 만든 뒤 Cookie 혹은 Session, DB 등에 저장하면 나중에 클라이언트가 요청을 보낼 때 CSRF Token을 비교하여 사용자 본인이 보낸 요청이 맞는지 확인할 수 있다.
import java.security.SecureRandom;
public class SecurityUtil {
// CSRF 토큰 생성
public static String generateCSRFToken() {
// 랜덤한 값 생성
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32];
random.nextBytes(bytes);
// token 문자열 생성
StringBuilder token = new StringBuilder();
for (byte b : bytes) {
token.append(String.format("%02x", b));
}
return token.toString();
}
}
// Servlet 예시
@WebServlet("/books/add")
public class AddBookServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// 페이지 접근
protected void doGet(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// CSRF 토큰 생성
String csrfToken = SecurityUtil.generateCSRFToken();
// cookie에 추가
Cookie cookie = new Cookie("csrfToken", csrfToken);
response.addCookie(cookie);
// view 반환
RequestDispatcher dispatcher =
request.getRequestDispatcher("/WEB-INF/views/addBook.jsp");
dispatcher.forward(request, response);
}
// 데이터 추가 요청
protected void doPost(
HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Cookie에서 csrfToken 탐색
Cookie[] cookies = request.getCookies();
String csrfToken = null;
for(Cookie cookie : cookies) {
if (cookie.getName().equals("csrfToken")) {
csrfToken = cookie.getValue();
break;
}
}
// DB나 Session 등에 따로 저장된 csrfToken
String savedCsrfToken;
// CSRF 토큰 검증
if (csrfToken == null || !savedCsrfToken.equals(csrfToken)) {
request.setAttribute("errorMessage", "잘못된 요청입니다.");
RequestDispatcher dispatcher =
request.getRequestDispatcher("/WEB-INF/views/error.jsp");
dispatcher.forward(request, response);
return;
}
// 검증 이후 동작 수행
}
}
SQL Injection 방지
- SQL Injection : 사용자 입력을 통해 SQL을 조작하여 Database를 공격하는 보안 공격이다.
- 테이블의 모든 정보를 가져오거나 테이블을 삭제하는 등의 공격이 있다.
공격 시나리오
- 예를 들어 사용자가 입력한 값을 그대로 SQL에 붙이는 경우
OR 1=1이 포함된 구문을 사용할 경우 테이블의 모든 행이 조회되어 사용자의 개인 정보가 탈취될 수 있다.
String userId = request.getParameter("userId");
String sql = "SELECT * FROM users WHERE userId = " + userId;
-- OR 1=1은 항상 true
SELECT * FROM users WHERE userId = 0 OR 1=1;
- 비슷하게
; DROP TABLE users;--와 같이 SQL문을 입력 값으로 보낸 경우 이를 SQL에 그대로 추가하면 테이블이 제거되어 Database가 손상될 수 있다.- SQL문을
;로 종료하고,--처리로 이후 구문을 주석 처리하는 방식이다.
- SQL문을
-- ; DROP TABLE users;--
SELECT * FROM users WHERE userId = 0; DROP TABLE users;--
SQL문 보호
- 입력 값을 SQL에 직접 추가하기 보다 입력 값에서 Database 자체나 테이블 조작을 시도하는 위험한 코드를 확인하고 제거하는 것이 좋다.
- java.sql 패키지로 제공되는 PreparedStatement 클래스의 메서드를 사용하는 방법도 있다.
PreparedStatement pstmt = conn.prepareStatement(
"SELECT * FROM users WHERE userId = ?");
pstmt.setInt(1, Integer.parseInt(userId));
- 악성 SQL문이 들어올 경우
?부분에 입력 값을 변환하여 넣으면 에러가 발생하여 SQL문이 실행되지 않는다.
SELECT * FROM users WHERE userId = 1